<!DOCTYPE html>
<html>
  <head>
    <title>about:memory</title>
    <script>
      document.addEventListener("DOMContentLoaded", start);

      function insertNode(root, report) {
        let currentNode = root;
        for (let path of report.path) {
          if (!currentNode[path]) {
            currentNode[path] = { total: 0, container: true };
          }
          currentNode = currentNode[path];
          currentNode.total += report.size;
        }
        currentNode.size = report.size;
        currentNode.container = false;
      }

      function formatBytes(bytes) {
        if (bytes < 1024) {
          return bytes + " B";
        } else if (bytes < 1024 * 1024) {
          return (bytes / 1024).toFixed(2) + " KiB";
        } else if (bytes < 1024 * 1024 * 1024) {
          return (bytes / (1024 * 1024)).toFixed(2) + " MiB";
        } else {
          return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GiB";
        }
      }

      function formattedSize(size) {
        // Use enough padding to take into account the "MiB" part.
        return formatBytes(size).padStart(10);
      }

      function convertNodeToDOM(node, name = null) {
        let result = document.createDocumentFragment();

        if (node.container) {
          let details = document.createElement("details");
          let summary = document.createElement("summary");
          summary.textContent = `${formattedSize(node.total)} -- ${name}`;
          details.append(summary);
          result.append(details);

          // Add the children in descending order of total size.
          let entries = Object.entries(node)
            .filter((item) => {
              return !["total", "size", "container"].includes(item[0]);
            })
            .sort((a, b) => b[1].total - a[1].total)
            .forEach((item) =>
              details.append(convertNodeToDOM(item[1], item[0]))
            );
        } else {
          let inner = document.createElement("div");
          inner.textContent = `${formattedSize(node.size)} -- ${name}`;
          result.append(inner);
        }

        return result;
      }

      function start() {
        window.startButton.onclick = async () => {
          let content = await navigator.servo.reportMemory();
          let reports = JSON.parse(content);
          if (reports.error) {
            console.error(reports.error);
            return;
          }
          window.report.innerHTML = "";
          window.report.classList.remove("hidden");

          let explicitRoot = {};
          let nonExplicitRoot = {};

          let jemallocHeapReportedSize = 0;
          let systemHeapReportedSize = 0;

          let jemallocHeapAllocatedSize = NaN;
          let systemHeapAllocatedSize = NaN;

          reports.forEach((report) => {
            // Add "explicit" to the start of the path, when appropriate.
            if (report.kind.startsWith("Explicit")) {
              report.path.unshift("explicit");
            }

            // Update the reported fractions of the heaps, when appropriate.
            if (report.kind == "ExplicitJemallocHeapSize") {
              jemallocHeapReportedSize += report.size;
            } else if (report.kind == "ExplicitSystemHeapSize") {
              systemHeapReportedSize += report.size;
            }

            // Record total size of the heaps, when we see them.
            if (report.path.length == 1) {
              if (report.path[0] == "jemalloc-heap-allocated") {
                jemallocHeapAllocatedSize = report.size;
              } else if (report.path[0] == "system-heap-allocated") {
                systemHeapAllocatedSize = report.size;
              }
            }

            // Insert this report at the proper position.
            insertNode(
              report.kind.startsWith("Explicit")
                ? explicitRoot
                : nonExplicitRoot,
              report
            );
          });

          // Compute and insert the heap-unclassified values.
          if (!isNaN(jemallocHeapAllocatedSize)) {
            insertNode(explicitRoot, {
              path: ["explicit", "jemalloc-heap-unclassified"],
              size: jemallocHeapAllocatedSize - jemallocHeapReportedSize,
            });
          }
          if (!isNaN(systemHeapAllocatedSize)) {
            insertNode(explicitRoot, {
              path: ["explicit", "system-heap-unclassified"],
              size: systemHeapAllocatedSize - systemHeapReportedSize,
            });
          }

          window.report.append(
            convertNodeToDOM(explicitRoot.explicit, "explicit")
          );

          for (let prop in nonExplicitRoot) {
            window.report.append(convertNodeToDOM(nonExplicitRoot[prop], prop));
          }
        };
      }
    </script>
    <style>
      html {
        font-family: sans-serif;
      }

      details,
      details div {
        margin-left: 1em;
      }

      summary:hover {
        cursor: pointer;
      }

      #report {
        line-height: 1.5em;
        border: 2px solid gray;
        border-radius: 10px;
        padding: 5px;
        background-color: lightgray;
      }

      #report > details {
        margin-bottom: 1em;
      }

      .hidden {
        display: none;
      }
    </style>
  </head>
  <body>
    <h2>Memory Reports</h2>
    <button id="startButton">Measure</button>
    <pre id="report" class="hidden"></pre>
  </body>
</html>
